8.2.2 定制的日志记录器

要想创建一个定制的日志记录器,需要创建一个 Logger 类型值。可以给每个日志记录器配置一个单独的目的地,并独立设置其前缀和标志。让我们来看一个示例程序,这个示例程序展示了如何创建不同的 Logger 类型的指针变量来支持不同的日志等级,如代码清单8-11所示。

代码清单8-11 listing11.go

01 // 这个示例程序展示如何创建定制的日志记录器
02 package main
03
04 import (
05   "io"
06   "io/ioutil"
07   "log"
08   "os"
09 )
10
11 var (
12   Trace  *log.Logger // 记录所有日志
13   Info  *log.Logger // 重要的信息
14   Warning *log.Logger // 需要注意的信息
15   Error  *log.Logger // 非常严重的问题
16 )
17
18 func init() {
19   file, err := os.OpenFile("errors.txt",
20     os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
21   if err != nil {
22     log.Fatalln("Failed to open error log file:", err)
23   }
24
25   Trace = log.New(ioutil.Discard,
26     "TRACE: ",
27     log.Ldate|log.Ltime|log.Lshortfile)
28
29   Info = log.New(os.Stdout,
30     "INFO: ",
31     log.Ldate|log.Ltime|log.Lshortfile)
32
33   Warning = log.New(os.Stdout,
34     "WARNING: ",
35     log.Ldate|log.Ltime|log.Lshortfile)
36
37   Error = log.New(io.MultiWriter(file, os.Stderr),
38     "ERROR: ",
39     log.Ldate|log.Ltime|log.Lshortfile)
40 }
41
42 func main() {
43   Trace.Println("I have something standard to say")
44   Info.Println("Special Information")
45   Warning.Println("There is something you need to know about")
46   Error.Println("Something has failed")
47 }

代码清单8-11展示了一段完整的程序,这段程序创建了4种不同的 Logger 类型的指针变量,分别命名为 TraceInfoWarningError 。每个变量使用不同的配置,用来表示不同的重要程度。让我们来分析一下这段代码是如何工作的。

在第11行到第16行,我们为4个日志等级声明了4个 Logger 类型的指针变量,如代码清单8-12所示。

代码清单8-12 listing11.go:第11行到第16行

11 var (
12   Trace  *log.Logger // 记录所有日志
13   Info  *log.Logger // 重要的信息
14   Warning *log.Logger // 需要注意的信息
15   Error  *log.Logger // 非常严重的问题
16 )

在代码清单8-12中可以看到对 Logger 类型的指针变量的声明。我们使用的变量名很简短,但是含义明确。接下来,让我们看一下 init() 函数的代码是如何创建每个 Logger 类型的值并将其地址赋给每个变量的,如代码清单8-13所示。

代码清单8-13 listing11.go:第25行到第39行

25   Trace = log.New(ioutil.Discard,
26     "TRACE: ",
27     log.Ldate|log.Ltime|log.Lshortfile)
28
29   Info = log.New(os.Stdout,
30     "INFO: ",
31     log.Ldate|log.Ltime|log.Lshortfile)
32
33   Warning = log.New(os.Stdout,
34     "WARNING: ",
35     log.Ldate|log.Ltime|log.Lshortfile)
36
37   Error = log.New(io.MultiWriter(file, os.Stderr),
38     "ERROR: ",
39     log.Ldate|log.Ltime|log.Lshortfile)

为了创建每个日志记录器,我们使用了 log 包的 New 函数,它创建并正确初始化一个 Logger 类型的值。函数 New 会返回新创建的值的地址。在 New 函数创建对应值的时候,我们需要给它传入一些参数,如代码清单8-14所示。

代码清单8-14 golang.org/src/log/log.go

// New创建一个新的Logger。out参数设置日志数据将被写入的目的地
// 参数prefix会在生成的每行日志的最开始出现
// 参数flag定义日志记录包含哪些属性
func New(out io.Writer, prefix string, flag int) *Logger {
  return &Logger{out: out, prefix: prefix, flag: flag}
}

代码清单8-14展示了来自 log 包的源代码里的 New 函数的声明。第一个参数 out 指定了日志要写到的目的地。这个参数传入的值必须实现了 io.Writer 接口。第二个参数 prefix 是之前看到的前缀,而日志的标志则是最后一个参数。

在这个程序里, Trace 日志记录器使用了 ioutil 包里的 Discard 变量作为写到的目的地,如代码清单8-15所示。

代码清单8-15 listing11.go:第25行到第27行

25   Trace = log.New(ioutil.Discard,
26     "TRACE: ",
27     log.Ldate|log.Ltime|log.Lshortfile)

变量 Discard 有一些有意思的属性,如代码清单8-16所示。

代码清单8-16 golang.org/src/io/ioutil/ioutil.go

// devNull是一个用int作为基础类型的类型
type devNull int
// Discard是一个io.Writer,所有的Write调用都不会有动作,但是会成功返回
var Discard io.Writer = devNull(0)
// io.Writer接口的实现
func (devNull) Write(p []byte) (int, error) {
  return len(p), nil
}

代码清单8-16展示了 Discard 变量的声明以及相关的实现。 Discard 变量的类型被声明为 io.Writer 接口类型,并被给定了一个 devNull 类型的值0。基于 devNull 类型实现的 Write 方法,会忽略所有写入这一变量的数据。当某个等级的日志不重要时,使用 Discard 变量可以禁用这个等级的日志。

日志记录器 InfoWarning 都使用 stdout 作为日志输出,如代码清单8-17所示。

代码清单8-17 listing11.go:第29行到第35行

29   Info = log.New(os.Stdout,
30     "INFO: ",
31     log.Ldate|log.Ltime|log.Lshortfile)
32
33   Warning = log.New(os.Stdout,
34     "WARNING: ",
35     log.Ldate|log.Ltime|log.Lshortfile)

变量 Stdout 的声明也有一些有意思的地方,如代码清单8-18所示。

代码清单8-18 golang.org/src/os/file.go

// Stdin、Stdout和Stderr是已经打开的文件,分别指向标准输入、标准输出和
// 标准错误的文件描述符
var (
  Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
  Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
  Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
os/file_unix.go
// NewFile用给出的文件描述符和名字返回一个新File
func NewFile(fd uintptr, name string) *File {

在代码清单8-18中可以看到3个变量的声明,分别表示所有操作系统里都有的3个标准输入/输出,即 StdinStdoutStderr 。这3个变量都被声明为 File 类型的指针,这个类型实现了 io.Writer 接口。有了这个知识,我们来看一下最后的日志记录器 Error ,如代码清单8-19所示。

代码清单8-19 listing11.go:第37行到第39行

37   Error = log.New(io.MultiWriter(file, os.Stderr),
38     "ERROR: ",
39     log.Ldate|log.Ltime|log.Lshortfile)

在代码清单8-19中可以看到 New 函数的第一个参数来自一个特殊的函数。这个特殊的函数就是 io 包里的 MultiWriter 函数,如代码清单8-20所示。

代码清单8-20 包 io 里的 MultiWriter 函数的声明

io.MultiWriter(file, os.Stderr)

代码清单8-20单独展示了 MultiWriter 函数的调用。这个函数调用会返回一个 io.Writer 接口类型值,这个值包含之前打开的文件 file ,以及 stderrMultiWriter 函数是一个变参函数,可以接受任意个实现了 io.Writer 接口的值。这个函数会返回一个 io.Writer 值,这个值会把所有传入的 io.Writer 的值绑在一起。当对这个返回值进行写入时,会向所有绑在一起的 io.Writer 值做写入。这让类似 log.New 这样的函数可以同时向多个 Writer 做输出。现在,当我们使用 Error 记录器记录日志时,输出会同时写到文件和 stderr

现在知道了该如何创建定制的记录器了,让我们看一下如何使用这些记录器来写日志消息,如代码清单8-21所示。

代码清单8-21 listing11.go:第42行到第47行

42 func main() {
43   Trace.Println("I have something standard to say")
44   Info.Println("Special Information")
45   Warning.Println("There is something you need to know about")
46   Error.Println("Something has failed")
47 }

代码清单8-21展示了代码清单8-11中的 main() 函数。在第43行到第46行,我们用自己创建的每个记录器写一条消息。每个记录器变量都包含一组方法,这组方法与 log 包里实现的那组函数完全一致,如代码清单8-22所示。

代码清单8-22展示了为 Logger 类型实现的所有方法。

代码清单8-22 不同的日志方法的声明

func (l *Logger) Fatal(v ...interface{})
func (l *Logger) Fatalf(format string, v ...interface{})
func (l *Logger) Fatalln(v ...interface{})
func (l *Logger) Flags() int
func (l *Logger) Output(calldepth int, s string) error
func (l *Logger) Panic(v ...interface{})
func (l *Logger) Panicf(format string, v ...interface{})
func (l *Logger) Panicln(v ...interface{})
func (l *Logger) Prefix() string
func (l *Logger) Print(v ...interface{})
func (l *Logger) Printf(format string, v ...interface{})
func (l *Logger) Println(v ...interface{})
func (l *Logger) SetFlags(flag int)
func (l *Logger) SetPrefix(prefix string)

results matching ""

    No results matching ""